Flint 0.5#9
Open
irzhywau wants to merge 559 commits into
Open
Conversation
…gine)
Make blast-radius a COMPUTED fact, not a self-rating. ReachDescriptorV1::derive
computes reach from the capsule's ENFORCED capability (isolation tier +
guest_network/carrier net access) and the affordance's concrete (resource,
operation) — never from the self-declared AffordanceRisk, which a capsule can set
to anything. This is the data the shell's halo will render, honest by
construction.
- ReachDescriptorV1 { egress(None|Open), isolation(Data|Wasm|MicroVm|HostProcess),
scope(Object|Collection|System|Unknown), reversibility(Reversible|OneWay|
Unknown), observed }. observed=false when an affordance is resource-less or
operation-less, so a vague act renders "incomplete", never falsely cool.
- declared_understates_reach(&AffordanceRisk): the advisory "a clone must lie"
detector — flags a capsule claiming low risk while the observed reach is far
(open egress, irreversible, or system-wide). Declared risk stays advisory; the
core stamps the observed reach and only flags the contradiction (does not gate).
- 6 tests: sandboxed read reaches nowhere; networked microVM delete over a
collection reaches far; a carrier service is a host process with open egress; a
scheme wildcard is system scope; resource/operation-less affordances are not
fully observed; declared-low-but-far-reaching is flagged.
Schema is V1 and additive — it is the shell's reach contract (and feeds ESP v0 in
W4). Next (W0b): wire derive() into the inspect/plan projection the shell reads;
W1 adds EgressReach::Allowlisted (per-host egress-as-capability).
Roadmap: W0 IN PROGRESS (engine landed). Gates: elastos-common 92 pass (6 reach
tests); clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ell reads
Every declared affordance in the capsule catalog now carries its CORE-DERIVED
reach, so a shell can render the blast-radius halo from real state rather than the
self-declared risk.
- AffordanceReachView { interface_id, method_id, risk, reach, declared_understates_reach }
is attached as a parallel `affordance_reach` list on the catalog entry,
computed in catalog_capsule_summary via ReachDescriptorV1::derive from the
manifest's isolation type + ENFORCED permissions and each method's concrete
(resource, operation). The manifest descriptor (CapsuleAffordanceDescriptor)
stays a pure declaration — reach is projected alongside it, never mutated in.
- The declared-understates-reach flag rides the projection, so the "a clone must
lie" contradiction (claims low, reaches far) is visible to whatever renders it.
Test: a capsule whose `scan.all` declares risk=read over `elastos://*` projects
egress=none / isolation=wasm / scope=system / reversibility=reversible /
observed=true AND declared_understates_reach=true; a contained `open.one` over a
specific resource projects scope=object and is not flagged.
Roadmap: W0 CLOSED (engine + projection). Next: W1 (egress-as-capability adds
EgressReach::Allowlisted + enforcement at the net boundary). Gates: elastos-server
lib 855 pass; clippy -D warnings + fmt clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ail-closed)
The reach vocabulary now distinguishes a LEASHED egress from a wide-open one —
the moat point: a verified capsule with Open egress can be visibly more dangerous
than one scoped to a handful of hosts.
- EgressReach gains Allowlisted (between None and Open).
- EgressAllowlist { allowed_hosts, allowed_schemes } with a fail-closed permits():
a (host, scheme) is permitted ONLY if both are explicitly listed; an empty
list permits nothing, so "no grant" and "grant nothing" both deny.
- EgressReach::resolve(has_net_capability, Option<&EgressAllowlist>) is honest and
fail-closed: no capability -> None; capability + non-empty allow -> Allowlisted;
capability + no/empty allow -> Open (surfaced as the dangerous reading, never
quietly downgraded).
- is_far_reaching already keys on Open only, so a leashed (allowlisted) egress is
correctly NOT far-reaching while an open one is.
HONEST SCOPE: this is the egress-capability MODEL + its fail-closed matcher.
Enforcing it at the kernel network boundary (the crosvm TAP firewall) and
threading a bound allow-list through the launch path is W1b — it requires
/dev/kvm + CAP_NET_ADMIN, which this sandbox host lacks, so it is deliberately
deferred to a host that has them rather than faked. WASM is already
default-no-internet by construction (no socket surface in its WasiCtx).
Roadmap: W1 IN PROGRESS (model landed; W1b = boundary firewall on the KVM lane).
Gates: elastos-common 95 pass (3 new egress tests); clippy -D warnings + fmt
clean; alignment-check OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…l token) The privileged shell session token is no longer tied to the magic capsule name "shell" — it goes to the user-selected ACTIVE shell, and only if that capsule holds the Shell role. - shell_token_eligible(name, role, active_shell) = role == Shell && name == active_shell. Fail-closed: a non-Shell capsule can NEVER receive the shell token even if the active-shell pointer names it. - Supervisor gains an `active_shell` pointer (default "shell" => behaviour-neutral) with set_active_shell()/active_shell(), so a different Shell-role capsule can be the active shell without the runtime hardcoding a name — the backend the shell-picker (W6) selects against. - The launch site swaps `name == "shell"` for the role-based decision; the bundled shell (role=shell) is unaffected. Test: the bundled shell stays eligible; a different Shell-role capsule selected as the active shell is eligible; a non-Shell capsule is never eligible; a Shell-role capsule that is not the active shell is not eligible. Deferred (W3b): rename the headless "shell" decision-engine to "consent-broker" (a wide, mechanical rename — safer as its own clearly-scoped slice). Roadmap: W3 IN PROGRESS (shell de-hardcoded; consent-broker rename = W3b). Gates: elastos-server lib 856 pass; clippy -D warnings + fmt clean; alignment-check OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…m shipped state ESP (the ElastOS Shell Protocol) v0: the read-only projection contract a shell renders. Written by EXTRACTION — every fact/verb cites the exact struct + route that already produces it; nothing is invented. - docs/ESP_V0.md: the inviolable read-only-projection rule; the real transport (HTTP GET JSON + the 202 consent flow); per-fact `schema`-tag versioning; must-ignore-unknown for FACTS with the honest caveat that verb INPUTS use deny_unknown_fields; the 5 fact families (catalog/v1, interfaces/v1, reach.v1, affordance-consent-pending/v1, affordance.receipt.v1) with routes + source structs; the 4 verbs (request/grant/deny/validate-and-consume + invoke); and an initialize handshake DEFINED but marked not-yet-implemented. Degraded surfaces called out: EgressReach::Allowlisted modeled-not-enforced (W1b), ReachDescriptor.observed, the redeem->dispatch live round-trip (integration-only). - elastos/esp/esp_v0.ts + README: shared TS types mirroring the serde shapes (snake_case enum wire values; optional = skip_serializing_if/default; fact types carry an index signature for must-ignore-unknown). Type-checks clean under `tsc --noEmit --strict`. - check-wci-alignment.sh: W4 ratchet — the schema tags + routes ESP_V0.md documents must still exist where it cites them, so the spec cannot silently drift from what the runtime serves. No Rust behaviour change (doc + types + script only) — elastos-server 856 / elastos-common 95 suites stand. Roadmap: W4 DONE; next W5 (the v1 projection shell + hero dDRM act). Gates: tsc --noEmit --strict clean; alignment-check OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…P-pure, in-cloud tested) The first pixel of the product, as verifiable projection logic: a headless, ESP-pure client that renders the never-seen two-channel object from REAL ESP v0 fact shapes — it holds no key and re-derives no crypto, it projects the runtime's verdicts (read-only projection principle). - two_channel.ts: Channel 1 trustMaterial(capsule) projects the runtime's trust_state verdict (verified | content_addressed | unsigned; fail-honest: unknown -> unsigned, never over-trusted). Channel 2 blastRadius(reach) projects the core-computed reach into a hazard band (open egress is high-hazard; a LEASHED/allowlisted egress reads cooler than an open one — the W1 point), honest about `observed` (unobserved -> incomplete, never falsely cool). twoChannel() composes them with refuseTrained (verified && hot) + declaredUnderstatesReach. - consent_act.ts: the hero dDRM act shapes — consentPendingIsWellFormed and receiptMatchesRequest (the receipt must attest the EXACT act requested; the runtime did the crypto, the shell asserts the binding). - 11 node:test cases prove it, headlined by "a VERIFIED capsule can read MORE dangerous than an UNSIGNED one" — you can refuse the thing the green check trained you to trust — and the hero act's receipt-binds-the-act safety. - Toolchain: tsconfig (strict + noUnusedLocals/Parameters + noImplicitReturns), a tiny node-shims.d.ts so it type-checks without @types/node, package.json (type: module), build/ gitignored. `tsc` clean; `node --test` 11/11. W5b (the visual Svelte shell) is the browser/local lane. No Rust change (esp_v0.ts gained the real trust_state/signature_state/cid_state fields) — the 856/95 suites stand. Gates: tsc strict + node --test 11 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…il-closed) "A shell among shells", made selectable — pure ESP projection over the catalog facts, no Rust change (the catalog already emits role + launchable). - shell_picker.ts: isShellSelectable(capsule) = role=="shell" && launchable — the ESP mirror of the runtime's shell_token_eligible, so a non-shell can NEVER become the active shell. selectableShells(catalog), shellTrustCard (reuses the two-channel trust-material so a user picks a shell knowing its trust verdict), shellPicker(catalog, active?) (defaults to first selectable; a stale/non-shell active falls back fail-closed), withActiveShell (returns null for a non-shell). - refraction.ts: RefractionState<T> — two registered shells over ONE projected state; toggleFocus swaps the lens and carries the projected state through UNCHANGED (one source of authority, no migration). The verifiable model under W5b's ~320ms visual cross-fade. - 21 node:test (with two_channel's): only shell+launchable selectable; non-shell never; picker default/select/fallback; the toggle preserves + round-trips the projected state. node-shims.d.ts gained deepEqual/strictEqual. The picker only RENDERS + REQUESTS; the runtime's set_active_shell (W3a) is the sole thing that actually switches authority (ESP read-only principle). No Rust change — the 856/95 suites stand. Gates: tsc strict + node --test 21 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…fact (the flywheel turns) The same signed receipt that delights the consumer IS the compliance evidence a regulator/insurer forces — re-projected, fail-closed. - ai_act_audit.ts: toAiActAuditRecord(consent, receipt) maps a redemption onto the controls an auditor checks -> AiActAuditRecordV1 (schema elastos.audit.ai-act.v1): the `act` (from the signed receipt), `human_oversight` (EU AI Act Art 14 — required for any user-approval OR high-risk class), `record_keeping` (Art 12 — the tamper-evident signed receipt), and a plain-language control mapping. - containmentEvidence(record): fail-closed at the audit layer — Art 12 fails on an UNSIGNED record (no receipt -> no provable act); Art 14 fails when a high-risk or user-approval act executed without human consent. `contained` requires both. - 26 node:test (with the rest of esp/): the mapping; a user-approved+signed act is contained; an unsigned record fails Art 12; a high-risk runtime-policy act (no human) fails Art 14; a low-risk automated act with a signed record is contained. This closes the in-cloud core of the full W0-W7 wedge sequence: consent is real and receipted (W2), the halo is truthful (W0), egress is modeled (W1a), the shell is de-hardcoded and selectable (W3a/W6), the protocol is written (W4), the two-channel object renders (W5a), and the receipt is now the compliance artifact (W7). Pure projection, no Rust change — the 856/95 suites stand. Gates: tsc strict + node --test 26 pass; alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ide shell rename) The consent decision-engine was implicitly conflated with the capsule role `Shell`: `shell_only_middleware` gated capability grant/deny/revoke + lifecycle endpoints by calling `session.is_shell()` directly. A naive wide "shell → consent-broker" rename was NOT cleanly shippable — the trust boundary is not a separable entity from the role (KNOWN_GAPS G-CIE: "the grant root IS the trusted shell"), so renaming the role is forbidden and renaming only the middleware would leave a misleading `consent_broker` gate still calling `is_shell()`. Instead, name the decision-engine at one documented seam: - add `Session::is_consent_broker()` — the consent-authorization predicate, delegating to `is_shell()` today with a doc bridge; a future dedicated consent-broker tier changes only this one method. - rename `shell_only_middleware → consent_broker_only_middleware`, routing it through `is_consent_broker()`; update the import + 4 application sites + 403 message + the 2 doc-comment references + the KNOWN_GAPS G-CIE note. Pure clarity, no behavior change: the predicate is byte-equivalent today, and a capsule session is still fail-closed NOT a consent broker (new unit test pins both the equivalence and the fail-closed case). The capsule role `Shell`, `active_shell`, and `shell_token_eligible` are untouched. Gates: rustfmt --check, clippy -D warnings (runtime+server), session/middleware/ capability tests green, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…on (DoS)
All three carrier bridges checked MAX_LINE_BYTES only AFTER `read_line` /
`.lines()` had already buffered the whole line, so an untrusted guest could OOM
the host by sending a line with no newline (the WASM API bridge had no bound at
all). The size check was decorative — the allocation it guarded already happened.
Add a fail-closed bounded reader that caps allocation DURING the read:
- `read_bounded_line` (async, AsyncBufRead) + `read_bounded_line_sync` (BufRead),
sharing a `BoundedLine { Line | Eof | TooLarge }` outcome. Each uses
`take(MAX+1).read_until(b'\n')` so the read itself stops at the cap, then
drains to the next newline IN BOUNDED CHUNKS (DRAIN_CHUNK_BYTES) so the stream
realigns to the following request without reintroducing the OOM.
- wired into all three loops: the async serial carrier bridge (the BUG-6-named
`read_line`), the sync WASM carrier bridge, and the sync WASM API bridge.
Test-first (in-memory pipes, no VM): `read_bounded_line_*` prove a complete line
reads, CRLF is stripped, an oversized line is rejected TooLarge AND the stream
realigns so the next request parses, oversized-at-EOF terminates cleanly, and the
sync twin enforces the same bound + realign.
Gates: cargo test (36 carrier_bridge tests green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-6 + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
`next_cid` advanced with an unchecked `*next += 1` and never consulted the live VM set, so once the counter wrapped (overflow) it could re-hand a vsock CID a running VM still held — two VMs sharing a CID breaks vsock routing. Replace with a pure `allocate_cid(from, in_use) -> Option<(cid, advance)>`: - floors the transport-reserved low CIDs (0/1/2) so guests start at MIN_GUEST_CID (3); - skips any CID currently in the `running` map's `vsock_cid` set; - wraps the u32 space via `checked_add().unwrap_or(MIN_GUEST_CID)` instead of overflowing; - bounded scan (`in_use.len() + 1` candidates — pigeonhole) and returns None to fail closed if every CID is occupied. The launch site now holds the counter lock across the live-set scan (serializes concurrent launches) and refuses the launch on None rather than colliding. Test-first (pure, VM-free): `supervisor::tests::allocate_cid_*` prove the reserved floor, skip-live, wrap-without-overflow, the wrap-does-not-collide-with- a-live-VM case (the bug), and free-within-bound. Gates: cargo test (29 supervisor tests green), clippy -D warnings, rustfmt --check. KNOWN_GAPS BUG-8 updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… final poll The carrier `request_capability` loop was sleep-then-check, so a grant recorded between the last in-loop poll and loop exit was never observed — the capsule got a spurious timeout even though the consent broker had granted. Extract `await_capability_decision` + a `CapabilityDecision` enum: poll the pending store on the interval, then do ONE final read after the loop so a late-landing decision is caught. The carrier bridge now routes its grant/deny/ expire/timeout handling through it (same observable responses). Test-first + deterministic (no timing flake): with `max_polls = 0` the loop body never runs, so a pre-seeded grant can ONLY be found by the trailing read — `await_capability_decision_catches_a_grant_after_the_loop` would time out under the old code. A clean-timeout case guards against a false grant. Residual (noted in KNOWN_GAPS): the WASM API bridge's HTTP poll has the same shape; its trailing-read fix needs an HTTP-mock test to gate and is a tracked follow-up (separate transport, not the named carrier loop). Gates: cargo test (38 carrier_bridge tests green), clippy -D warnings, rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-5 + ROADMAP updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…g failure
A single-use capability is consumed at validate() before send_raw, so a provider
failure burned the grant on a no-op (the holder had to re-consent for a failure
that wasn't their fault).
An object-capability audit established that refunding on a general provider Err
is UNSAFE: no carrier provider op is atomic on its Err path (write-then-fail is
possible) and ProviderError::{Provider,NotFound,Io} cannot distinguish "acted
then failed" from "never acted" — so a refund there could enable a second
execution of a partially-applied write (catastrophic for the actuator/payment
classes that are the point of the AI-Act containment story).
Land the one PROVABLY-safe slice: refund ONLY on ProviderError::NoProvider (a
routing failure — the registry invoked nothing), via a new saturating
CapabilityStore::refund_token_use + CapabilityManager::refund_use. Every other
failure keeps the use consumed (fail-closed). The carrier dispatch binds the
consumed token id once (single-assignment; we only reach dispatch with a
validated, consumed token) and refunds it on the NoProvider arm.
Test-first:
- capability::store::refund_token_use_is_the_saturating_inverse_of_try_use —
the core mechanic (returns exactly one slot; saturates at 0, never mints uses).
- carrier_bridge::carrier_invoke_refunds_single_use_grant_on_missing_provider —
the SAME single-use token reaches the provider lookup TWICE through the public
handle_request path; under the old burn-on-failure code the second call would
be capability_denied.
Residual (deferred, bigger refactor, recorded in KNOWN_GAPS): op-failure refunds
need a provider "did-not-act" signal before they can be made safe.
Gates: cargo test (39 carrier_bridge + 5 store green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-4 + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Define docs/ESP_SHELL_UI.md: every Svelte component the shell needs, mapped onto the proven, tested ESP projection it renders — the UI adds no new logic, it only projects what the headless esp/*.ts layer already computed (W5a/W6/W7). - The one invariant (every pixel a read-only projection of signed runtime state; no component holds a key, token, or authority; actions emit INTENTS the runtime gates, never authority the view mints). - Eight component contracts (props = ESP fact type + schema tag; the headless fn it projects; what it renders; the intent it may emit): CapsuleCard, TwoChannelObject (refuseTrained = verified && hot), HazardMeter, ConsentSheet, ReceiptBadge, ShellPicker, RefractionToggle, AiActAuditCard (Art 12/14 contained). - Composition tree + a schema-tag pinning table so the alignment ratchet can later guard the pixel⇄fact mapping, mirroring the W4 ESP route pins. Alignment ratchet gains W5b pins (the AiActAuditCard fact tag must exist in esp/ai_act_audit.ts; the spec must define the card and codify the no-authority invariant). The live Svelte paint + visual snapshot test stay on the browser/ local lane (tracked as W5b implementation in ROADMAP). Gates: WCI alignment OK (new W5b pins pass); ESP TS unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
A team code-quality audit (Kent Beck seat) over the campaign diff, each finding adversarially verified before applying: - F1 (confirmed): the `request_too_large` reply was hand-built in all three bridge loops with drifting formatting. Extract one `oversized_request_error()` so the wire shape cannot diverge between the microVM, WASM-carrier, and WASM-API bridges. - F2 (already satisfied): the sync bounded reader already documents itself as the "synchronous twin of read_bounded_line … same semantics" — the divergence-catching cross-reference the finding asked for. No change needed. - F3 (corrected): the finding claimed the WASM-API HTTP poll has "no bug — the last iteration is the final check." That premise is FALSE: the loop shares BUG-5's post-last-poll window (it is the tracked BUG-5 residual). Added an HONEST comment pointing at KNOWN_GAPS, NOT the finding's incorrect "fine" framing. No behavior change. Gates: cargo test (39 carrier_bridge green), clippy -D warnings, rustfmt --check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… safe refund Close the BUG-4 residual's MECHANISM: providers can now signal a provably-pre- effect rejection, and the carrier safely refunds the single use on it. - Add `ProviderError::DidNotAct(String)` with a load-bearing ocap contract on the variant: a provider may return it ONLY when it is certain nothing was mutated/ emitted/spent (precondition/validation failure), so a replay is an idempotent no-op. Any failure that MIGHT have partially acted must keep using Provider/Io/ NotFound so the use stays consumed. - The carrier refunds the consumed single use on `DidNotAct` (mirroring the `NoProvider` slice); every other Err keeps it consumed (fail-closed). Only the two provably-no-effect classes are refund-safe. - Wire the new variant through the two exhaustive matches: `Display` and the storage `From<ProviderError>` (→ 400-class InvalidPath). Test-first (mock providers, no production behavior change): - carrier_invoke_refunds_single_use_grant_on_did_not_act — a DidNotAct failure refunds, so the SAME single-use token reaches the provider twice. - carrier_invoke_keeps_single_use_consumed_on_acted_failure — an acted Provider failure keeps the use consumed (second call capability_denied). Real-provider migrations (e.g. read-only InspectProvider) are a tracked follow-up in KNOWN_GAPS with a per-op "provably no side effect" gate + a response-shape caveat — not a blanket sweep. Also records the G3b decision in KNOWN_GAPS: keep preview/enforce as DISJOINT tables (cross-checked), NOT a shared table (which would make the conformance test vacuous) — correcting a swarm proposal. Gates: cargo test (871 server lib + 33 provider green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ider manifests Generalize the single rights fixture to a UNIVERSAL pin: enumerate every shipped provider manifest (22 with authority) and assert, for every declared op, that the verb-map-ENFORCED action (`required_action_for`) is a MEMBER of the manifest's PREVIEWED action set (`plan_provider_operation`). The two tables stay DISJOINT (no shared function), so drift fails loudly — Option B, not a shared table (which would make the test vacuous). Key correction during build: a naive `previewed == enforced` is wrong universally — a multi-action capability block legitimately previews a union, so the right invariant is membership (the manifest must declare AT LEAST the enforced action); a true drift is enforcement requiring an action the manifest never grants for the op (a verb-map Admin fallthrough). The pin surfaced 49 real drifts (all fail-CLOSED today: previewed-but-denied, never an escalation). A 4-agent classification swarm triaged them into a documented `known_divergences` ledger (read-safe / write / execute-egress / manifest-over-declares / HIGH-RISK-keep-Admin). The ledger is a ratchet: a NEW drift fails the test, and removing a fixed op without a real fix also fails (it cannot rot). Per-op RESOLUTION (verb-map entries for safe reads; restructure blanket-`execute` manifests like did/*; KEEP Admin for wallet-signing / key-release / decrypt / seal / broadcast) is tracked follow-up — deliberately NOT a bulk enforcement loosen. This CLOSES the G3b "drift can hide" gap. KNOWN_GAPS G3 row updated. Gates: cargo test (rights + universal conformance green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ller refactor Verified the response-shape gate the KNOWN_GAPS caveat required before migrating InspectProvider to emit ProviderError::DidNotAct. Result: NOT a drop-in. Its send_raw Ok(error-status Value) contract is consumed by BOTH transports (the carrier AND the gateway provider proxy, gateway_provider_proxy.rs:1399) plus ~21 inspect-test assertion sites. Emitting Err(DidNotAct) would change the response shape on both transports and break those assertions — a scoped refactor, not a quick PoC. Recommendation recorded: the first real DidNotAct migration should target a carrier-only provider (no gateway dependency on its send_raw error shape), or do InspectProvider as its own chunk that updates the gateway proxy + tests. The DidNotAct mechanism itself stays proven by last turn's mock-provider PoC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…cked ops
Shrink the G3b preview≠enforce ledger by 11, the swarm-verified subset where the
op was UNMAPPED (→ fail-closed Admin) but its shipped manifest ALREADY declares
the matching action — so adding the verb-map entry makes preview==enforce and
loosens enforcement only to the action the author already authorized (a pure
preview-honesty fix, no new authority).
Graydon-seat verification read each impl and confirmed the action class:
Read: download, events, metadata_index, read_bytes, reconstruct_listing,
export_graph, prepare_publish (pure assembly — "NEVER published")
Write: restore, write_bytes, import_graph
Delete: empty_trash (fs::remove_dir_all)
object-provider `share` deliberately STAYS in the ledger (grants access —
security-touching, held for a dedicated review). The ratchet did its job: it
flagged `prepare_publish` as healed-but-still-listed until I removed it, so the
ledger cannot rot.
Gates: cargo test (872 server lib, incl. universal conformance, green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…/ai/llama/tunnel)
Drain 12 more G3b drifts by making the manifests HONEST: the blanket
single-action capability blocks declared one action for ops the verb map enforces
at finer granularity, so preview≠enforce. Split each into per-action blocks that
match the (unchanged) verb-map enforcement — a preview-honesty change that grants
no new authority and drops no op (Miller-seat verified every op's action class).
- did-provider: [execute]* -> read{get_did,resolve,get_nickname,get_persona_did}
+ write{set_nickname} + execute{sign_chat_message,verify,verify_did_recovery}
- ai-provider: [execute]* -> read{list_backends,ping} + execute{chat_completions}
- llama-provider: [execute]* -> read{status,health,list_models} + execute{chat_completions}
(health/list_models verb-map Read added in ch1)
- tunnel-provider:[admin]* -> read{status,ping} + admin{start,stop}
Same resource/reason/audit_events preserved per manifest. The conformance ratchet
again caught healed-but-still-listed ops (did/*) until removed from the ledger.
Tracked separately (Miller): did `get_persona_did` now previews Read (matching the
EXISTING Read enforcement), but its impl create-on-first-call has Write semantics —
a pre-existing verb-map question, not a G3b drift.
G3b drain now 23/49 resolved; 26 sensitive-tail drifts remain (egress/actuator/
keys/signing — KEEP Admin).
Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… splits
Drain 8 more G3b drifts. Two egress-security agents read the net-provider and
exit-provider implementations and confirmed: their egress ops (connect/stream/
http; quote/open_stream/close_stream/http_fetch) are genuinely Execute, and
NEITHER provider has any operation that mutates persistent state — so the manifest
`write` action was dead/over-declared.
- net-provider: [read,write]* -> read{status,resolve} + execute{connect,stream,http}
- exit-provider: [read,write]* -> read{status} + execute{quote,open_stream,close_stream,http_fetch}
(dead `write` dropped from both; enforcement unchanged — pure preview honesty)
- encrypt-provider: [write]{status,seal} -> read{status} + write{seal}
`status` drift fixed; `seal` STAYS in the ledger (HIGH-RISK: CEK sealing,
enforced Admin) for a dedicated review.
G3b drain now 31/49 resolved; 18 dangerous-tail drifts remain at Admin/tracked
(browser-actuator, drm open, encrypt seal, wallet signing/approval/secret-export,
key release, decrypt, chain broadcast, object share).
Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Drain 5 more G3b drifts. A UI-actuation security agent read the
browser-engine-adapter implementation and confirmed: launch/attach_stream/
close_page/input/webrtc_signal are Execute (UI actuation — driving a headless
browser page), sandbox-scoped, with NO host command injection and NO secret
custody, so Execute (not Admin) is correct; status/page_status/screenshot/frame
are Reads; and NO op mutates persistent state (the manifest `write` was dead).
browser-engine-adapter: [read,write]* ->
read{status,page_status,screenshot,frame} +
execute{launch,attach_stream,close_page,input,webrtc_signal}
(dead `write` dropped; enforcement unchanged — pure preview honesty)
G3b drain now 36/49 resolved; 13 dangerous-tail drifts remain at Admin/tracked
(drm open, encrypt seal, wallet signing/approval/secret-export, key release,
decrypt, chain broadcast, object share).
Gates: cargo test (872 server lib green), clippy -D warnings, rustfmt --check,
WCI alignment OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…d teeth
W1b design contract (KVM-lane build): a per-capsule kernel firewall on the
capsule's TAP that physically drops egress outside its declared EgressReach,
fail-closed, with every blocked attempt written to the signed flight recorder.
- Keys on the now-honest egress capability (G3b drain: net/exit declare `execute`)
and the existing reach vocabulary (EgressReach::{None,Allowlisted,Open},
EgressAllowlist::permits, EgressReach::resolve).
- Threads the EXISTING TAP launch hook (supervisor.rs:1115, guest_network) — install
a per-TAP nftables chain (None=drop-all, Allowlisted=allow-resolved-only,
Open=allow+tagged-hot); tear down on reap (leak-free, mirrors BUG-2/3).
- Names the DNS problem honestly (host-scoped allowlist vs IP filter) with a
host-side DNS-proxy primary + IP-set interim.
- Spells out the privilege requirements (/dev/kvm + CAP_NET_ADMIN + nftables) — why
it is the KVM lane, not in-cloud — plus a 5-test plan incl. a compromised-guest
backstop.
The convergence: reach model + honest egress capability + flight recorder become
one enforced thing — packet-level proof the agent stayed in its lane (EU AI Act
containment). Alignment pins anchor the doc to the reach API + TAP hook it builds on.
Gates: WCI alignment OK (new W1b pins pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ovider The DidNotAct mechanism was proven last turn with mock providers; this is the first REAL provider to adopt it. A swarm confirmed CarrierGossipProvider (the `peer` scheme) is carrier-only — no gateway HTTP path consumes its send_raw error shape — so changing the error contract is safe. gossip_join/gossip_leave now return `ProviderError::DidNotAct` for the pre-effect empty-topic rejection (the request-shape check runs BEFORE any join/remove/gossip), so the carrier refunds the consumed single-use capability for that no-op. The `join_failed` path stays `Provider` (the join may have partially acted) — only the provably-pre-effect rejection is refund-safe. Proven against the REAL provider: carrier::tests:: gossip_join_and_leave_empty_topic_return_did_not_act builds a live CarrierGossipProvider and asserts both ops return DidNotAct on an empty topic. No existing test asserted the old `missing_topic` Ok-shape (verified). Gates: cargo test (carrier module 44 green), clippy -D warnings, rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-4 follow-up updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
The WASM-API bridge's HTTP capability poll (handle_remote_request) shared BUG-5's shape — a grant landing between the last GET and the timeout was dropped to a spurious "still pending". (The in-process carrier loop was already fixed via await_capability_decision's trailing read.) Extract a transport-agnostic `poll_then_final_read(max_polls, poll_ms, poll)` — poll on the interval, then ONE final read — and route the HTTP loop through it (the GET+classify becomes the `poll` closure; granted/denied/expired/pending map to RemoteCapabilityPoll). Same observable responses; the late-grant window closes. Tested deterministically WITHOUT a flaky HTTP mock: poll_then_final_read runs with max_polls = 0 (loop skipped) so ONLY the trailing read can return the decision — the exact structural gap the old loop had — plus a pending→timeout case. The test exercises the same loop structure the HTTP path now uses. Gates: cargo test (874 server lib green), clippy -D warnings, rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-5 residual marked CLOSED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Drain more refundable pre-effect rejections, all swarm-verified carrier-only and pre-effect (no test asserts the old shapes). - CarrierAvailabilityProvider (availability, carrier-only): the request-shape validations now return `DidNotAct` — missing/invalid `cid` on ensure/repair, and missing/invalid `cid` + invalid `path` on fetch — so the carrier refunds the unused single-use on these provable no-ops. The post-lock `join_failed` stays a structured error (the join may have acted). - CarrierGossipProvider gossip_join: the `already_joined` and `too_many_topics` no-op rejections (state read-only, nothing joined/sent) now return `DidNotAct`. Miller-seat audit CAUGHT the one unsafe candidate: gossip_leave's `not_joined` check runs AFTER three speculative `remove()` calls, so it stays a structured `Ok` error (not refund-safe). Availability's `too_many_topics` capacity rejections are verified pre-effect but held as a category decision (capacity, not request shape) — noted in KNOWN_GAPS. Test-first (real providers): carrier::tests:: carrier_availability_request_shape_rejections_return_did_not_act asserts missing-cid (ensure+fetch) and an invalid cid return DidNotAct; the gossip empty-topic test already covers that provider. Gates: cargo test (875 server lib green), clippy -D warnings, rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-4 follow-up updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ovider fetch A swarm + ocap review sharpened the DidNotAct contract: a single-use refund is safe ONLY when nothing acted AND a REPLAY is a guaranteed no-op — a property of the REQUEST (missing/invalid/unsupported field) or of a STABLE state, NOT a TRANSIENT capacity/quota condition (a replay could ACT once capacity frees, so refunding would let one consent drive an effect on retry). Self-correction: the previous chunk's `gossip_join` too_many_topics migration was too broad — reverted to a structured error (kept `already_joined`, a stable no-op). The ProviderError::DidNotAct doc now states the replay-is-a-guaranteed- no-op rule and calls out capacity rejections explicitly. Third real migration: ContentProvider (`content`, carrier-only) `fetch` returns DidNotAct for its request-shape rejections (missing/invalid cid, invalid path — all pre-effect, before any registry/IPFS fetch). The existing content_fetch_rejects_invalid_cid_and_path test (real provider) is updated to assert DidNotAct, and adds the missing-cid case. The post-validation fetch (may fail mid-stream) stays Provider/Io. Further drainable (noted in KNOWN_GAPS): import_exact/import_object/publish request-shape validations follow the same pattern. Gates: cargo test (server 875 + runtime 345 green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ons + Cursor handoff Continue the in-cloud BUG-4 drain (the lane available while the operator is away): - ContentProvider (carrier-only) write paths now return DidNotAct for their pre-effect request-shape rejections — import_exact invalid cid and publish file missing data — both reject BEFORE the IPFS add, so a replay is a deterministic no-op and the carrier refunds the unused single-use. The storage_quota_exceeded check correctly STAYS a structured error (capacity is transient, per the refined DidNotAct contract — a nice confirmation the contract refinement holds). Verified each migrated site is pre-effect by reading the code (write path — extra care: a mistake would refund after a partial write). - Test (real provider): content_write_request_shape_rejections_return_did_not_act. Add docs/READY_FOR_CURSOR.md — the clean local/KVM-lane pickup checklist (W1b egress firewall, BUG-2/3/7 VM-lifecycle leaks, W5b Svelte shell) with the verify commands and the standing gated-chunk discipline, so the hardware-dependent work is a clean hand-off. Gates: cargo test (876 server lib green), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Complete the ContentProvider publish op's request-shape drain: `unsupported_content_kind` (missing/unknown kind) and directory `files-must-be-array` now return DidNotAct — both reject pre-effect (before the IPFS add), so a replay is a deterministic no-op and the carrier refunds the unused single-use. Test extended to cover both. With this, the clearly-pre-effect single-site request-shape rejections across all three carrier-only providers (gossip, availability, content) are DRAINED — the in-cloud BUG-4 frontier is effectively complete. The only remaining DidNotAct candidates are helper-gated import_exact validations (shared-helper contract change — a wider, lower-value edit), noted in KNOWN_GAPS. Verified the WHOLE workspace is green (cargo test --workspace --lib: every crate, 0 failures). Gates: cargo test (workspace lib all green; server 876), clippy -D warnings, rustfmt --check, WCI alignment OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
The handoff covered the W0-W7 hardware tail (W1b, BUG-2/3/7, W5b) but was not the complete remaining registry. Close that gap: - Add the env-dependent-test caveat (the ~9 browser-engine + checksum integration tests that only pass on real Linux — NOT regressions; the --lib suites are the clean in-cloud signal). - Add the COMPLETE remaining registry pointing to ROADMAP (DEFERRED/TRACKED + LOCAL/CURSOR) + KNOWN_GAPS as the authoritative sources: AUD-1 activation, carrier-service author gate, AUD-4 plane-(a), G8/G8b audit-plane wiring, G1b/G2b serve-wiring, the perf reflink/group-commit wins, the 13 deliberately- locked G3b drifts, and the helper-gated DidNotAct tail. - Add the macro vision's NEXT band (PDR product roadmap beyond W0-W7): dual-receipt platform self-attestation, act-over-MCP write path, the spend meter, free-text NL→intent, the marketplace of shells — none started here, all tracked. So a Cursor pickup sees the full surface, and W0-W7 (the foundation) is clearly distinguished from the larger product vision still ahead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… chain
Closes the S37/S39-ledgered hung-read residual at its root. run_chain_capsule
(the ONE shared chain-provider subprocess helper — rights gate, resolve,
quote, buy prepare/broadcast, receipt/confirmation reads) now bounds the
whole conversation: ELASTOS_CHAIN_READ_DEADLINE_SECS (default 30; malformed
or <1 => the default, loudly), child spawned in its own process group, a
watchdog SIGKILLs the group at expiry, the child is always reaped.
The money line (both council seats verified SAFE at every consumer): a
deadline error carries CHAIN_DEADLINE_MARKER + "UNRESOLVED". On the SEND leg
it classifies INDETERMINATE (the tx may have broadcast — hold, reconcile),
NEVER a refund. On read legs it is an ordinary fail-closed refusal/hold.
Council: two seats, both SHIP-WITH-FIXES; all findings folded:
- pid/pgid-reuse kill window (red-team F1 / guardian F3): the watchdog is
disarmed and joined BEFORE the child is reaped, so it can only ever kill a
still-live, un-reaped child.
- unbounded response-line length (red-team F2): read_capsule_line is now
length-capped (MAX_CAPSULE_LINE = 4 MiB), so a firehose provider is a
bounded error, not an OOM.
- hostile-provider refund break (red-team F3, HIGH, pre-existing): a
`chain-provider op failed:` error is post-broadcast by construction, so
is_pre_broadcast_refusal now refuses to read any op-failed error as a
pre-broadcast refusal (=> Indeterminate/hold).
- error honesty (guardian F2): the deadline error keeps the underlying error
and `fired` is set only inside the unix kill.
- doc over-claim (guardian F1): the deadline bounds ONE chain conversation;
the wallet-provider sign leg and rights-provider decide leg are separate
undeadlined subprocesses — the tracked sibling residual. Plus the
availability-cliff note and comment-precision nits (F4/F5).
Ratchets: rights_authority::{a_hung_chain_provider_is_killed_at_the_deadline,
a_malformed_deadline_env_keeps_the_default_protection},
drm_marketplace::{a_chain_deadline_error_is_never_classified_as_a_refund,
a_post_broadcast_op_error_is_never_read_as_a_pre_broadcast_refusal}.
Gate: runtime 434 + server 1260 + common 103 green; dev-modes green; clippy
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…unded reap
Factor Sprint 40's chain-conversation watchdog into ONE shared module
(api/capsule_watchdog.rs) and extend the deadline discipline to the two
remaining unbounded subprocess conversations a money/access path traverses:
- wallet-provider SIGN leg (WalletSession::exchange): a timeout is a
PRE-broadcast refusal — the tx was never signed, so provably NotCharged.
drm_marketplace::is_pre_broadcast_refusal recognizes the marker ⇒ refund,
the exact mirror of the chain send-leg (Indeterminate/hold) rule.
- rights-provider DECIDE leg (run_rights_capsule): a timeout DENIES access
(fail-closed — every access consumer denies on Err).
The shared module owns the subtle bits so no third copy can drift: the
process-group spawn, the disarm-before-reap ordering (a recycled pid can
never take a stray group kill), the length-capped read, and the shared
deadline knob.
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- guardian F1 (HIGH) + F2: the disarm-before-reap left the REAP itself
unbounded — a provider that answered every op then refused to exit on
EOF/shutdown would park the runtime thread forever on child.wait(), the
very hang the deadline exists to end, slipped past the read into the reap.
New reap_grouped() gives a brief grace then group-SIGKILLs an un-exiting
child; wired at all three sites (chain, rights, wallet shutdown).
- guardian F3 (P12): scoped the "every money/access boundary is bounded"
doc claim to the three provider conversations actually bounded, restored
the unix-only qualifier, and registered the access-path sidecar residual.
- guardian F4: added the live-kill ratchet the money-critical sign leg
lacked (a_hung_wallet_provider_is_killed_and_classified_pre_broadcast),
asserting bounded return + marker + end-to-end refund classification.
- guardian F5: a watchdog fire now poisons the session so no later leg runs
on the dead child (a fired-but-read-Ok race can no longer degrade a
provable refund to a stranded Indeterminate hold); deadline errors append
the underlying read error for diagnostics.
- guardian F6 / red-team F1: honesty comments — the marker names the
session's purpose (any pre-broadcast leg), the wallet read is now capped,
and a footgun note that a future human-consent approve must run OUTSIDE
this deadline.
- red-team F2: DeadlineWatchdog now disarms on Drop, so an un-disarmed
watchdog is a no-op rather than a latent full-deadline group-kill.
Gate: server lib 1263 (default) / 1264 (dev-modes), runtime 434, common 96 —
all green; every new ratchet returns bounded (~1s, not the stub's 300s).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ng open/view
Close the last residual the Sprint 41 guardian left open: the access-path
sidecar helper reads (the media/object content authorities and the trustless
grant sidecar) were still unbounded, so a hung or hostile content-authority
subprocess could park a request thread forever. This wires those reads onto
the EXISTING shared capsule_watchdog module (no fourth copy of the ordering)
and adds the shared pieces they need:
- ReapGuard: a spawned child that is BOUNDED-reaped on drop unless disarmed,
so EVERY early return between spawn and the point the child is handed to
its owner reaps it (no leaked live process, no zombie).
- read_line_deadlined / read_open_deadlined: per-op VIEW reads use the short
shared deadline; content-OPEN descriptor reads use a new, longer
content_open_deadline (ELASTOS_CONTENT_OPEN_DEADLINE_SECS, default 120)
because a media open runs ffmpeg + a seal handshake before it answers.
- read_to_eof_deadlined: the ONE bounded one-shot read (the grant sidecar).
- ACCESS_DEADLINE_MARKER: a content-authority timeout is a DENY (fail-closed)
— the mirror of the rights-decide rule, NEVER a money decision (these sit
on the open/view paths, not the pay spine).
Applied to object_authority (both descriptor reads + the object/page VIEW
reads + grouped spawn + bounded Drop), media_authority (both descriptor reads
+ the segment VIEW read + grouped spawn + bounded Drop, its local reaper
replaced by the shared ReapGuard), and access_grant (the one-shot run_sidecar).
Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
- RT-F1 (HIGH) / guardian-1: object_authority leaked a LIVE child on a
well-formed-but-incomplete descriptor (field-extraction `?` after a good
read). Closed by the shared ReapGuard — every early return now reaps.
- RT-F2 (MED-HIGH): access_grant leaked the child on the stdin-write (EPIPE)
path — the reap ran only after the read. Closed by the same guard.
- RT-F3 (MEDIUM): the RPC-tuned 30s deadline silently capped an ffmpeg open
(an availability cliff). Split into the separate content_open_deadline.
- RT-F4 / guardian-2: child_pid() returned 0 → a fire would kill(-0) the
gateway's OWN process group. ReapGuard::pid panics instead, and arm now
refuses to kill on pid 0.
- guardian-3: access_grant hand-rolled a fourth copy of the arm/read/disarm/
reap ordering. Factored into the shared read_to_eof_deadlined (which also
now carries the marker, closing the doc over-claim).
- guardian-4: reap_grouped returns the ExitStatus so the one-shot folds a
non-success exit into its error.
- guardian-5/6/7/8: fmt fix; a hung-VIEW-read ratchet (the per-op leg,
distinct from the open); the write-leg-is-sub-pipe-buffer note; the stale
reap-backstop comment corrected; the prepare/assemble spawn_blocking
follow-up registered in KNOWN_GAPS.
Ratchets (all bounded ~1-2s vs a 300s stub): a hung media/object authority and
a hung grant sidecar are each killed and DENIED, and a sidecar that answers the
descriptor then hangs on a VIEW read is separately bounded.
Docs: the S41 sidecar residual is now CLOSED in KNOWN_GAPS, DRM_MARKETPLACE_RAIL
and FLINT_MANDATE_ENGINE — every content-open/view + provider conversation the
pay/access pipeline traverses is bounded, reap included, kill unix-only.
Gate: server lib 1267 (default) / 1268 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean on the touched files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lassifier
The last soft spot in the money classifier: `drm_marketplace::is_pre_broadcast_refusal`
decided refund-vs-hold by matching SUBSTRINGS of `buy_access`'s error string —
provider-controlled text touching a money decision. This makes the decision a
TYPE, not a string.
- `buy_access` now returns `Result<BuyOutcome, BuyError>` where
`BuyError::{PreBroadcast, Indeterminate}` is decided BY CONSTRUCTION: each
error site is mapped by its position relative to the broadcast op. Every
`broadcast_signed_*`/`broadcast_mock` call and every post-broadcast op is
`.map_err(Indeterminate)`; every pre-send leg (wallet-not-linked, source,
sold-out, drift, the wallet SIGN leg, the chain PREPARE read, the missing-
signature precondition, the dev record) is `.map_err(PreBroadcast)`.
- `DrmSettleError::from_buy_error` maps the variant. The string classifier
(`is_pre_broadcast_refusal` + its `for_test` bridge + the pre-broadcast
sentinel list + the `chain-provider op failed` exclusion) is DELETED.
- There is NO `From<String> for BuyError`, so a bare `?` on a broadcast call
will not compile — a future refactor cannot silently mis-map a send error.
- Side benefit the type unlocks: a chain deadline on the PREPARE (read) leg is
now correctly refundable while the same marker on the SEND leg stays held —
a distinction the string could not draw (all deltas are safe-or-refund).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed the invariant — no provider input can refund a possibly-sent
tx — holds by construction):
- guardian F1 (must-fix, P12): rewrote three wallet_signer doc/test comments
that still pointed at the deleted classifier — the wallet marker is now
diagnostic text, classification is the call-site variant.
- guardian F2 (P12 honesty): the prepare-leg refinement is transitively
covered by the sign-leg call-site mapping; reworded KNOWN_GAPS so "Proven
by" no longer adjoins an unratcheted claim, and registered the dedicated
prepare-leg fixture as a follow-up.
- guardian F3: added ratchets for the two previously-untested variant sites —
a dev record failure types PreBroadcast (a behavior change the typing makes
correct) and a post-broadcast record failure types Indeterminate.
- guardian F4 (P16): documented the trusted-binary caveat on the PreBroadcast
variant (the runtime owns the only broadcast; env allowlist is the follow-up).
- red-team INFO-2: removed the dead `needs a signed transaction` phrase check.
Retired ratchet intents preserved and strengthened: the hostile-provider proof
is now `from_buy_error_classifies_by_variant_not_by_string` (an Indeterminate
error embedding EVERY sentinel + both deadline markers still holds) plus the
end-to-end `a_broadcast_op_error_types_the_buy_as_indeterminate_even_with_a_sentinel`.
Gate: server lib 1264 (default) / 1269 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…RM 2d
The DRM confirmation reconciler used to identify its pending records by
sniffing `rail_note` for a `drm:tx=` prefix, so a hostile HTTP payment
endpoint whose Indeterminate body began `drm:tx=` could get its pending
polled/resolved by the DRM driver (council S35 red-team F5, fail-closed
direction but wrong). This replaces the note-sniff with a STRUCTURED tag.
- `PaymentRail::{Unknown, Http, Drm}` on `PaymentRecord`, stamped from the
paying provider at `begin_attempt` via a new REQUIRED `PaymentProvider::
rail()` method (no default — the tag is now a COMPILE-TIME property, the
S43 type-over-comment lesson). `DrmMarketplaceProvider::rail()==Drm`,
`HttpPaymentProvider::rail()==Http`.
- `reconcile_drm_confirmations` selects via `is_drm_pending`: Drm→ours,
Http→never (regardless of note), Unknown→bounded pre-S44 note fallback.
A positively-tagged Http pending — even one whose body a hostile endpoint
crafted to begin `drm:tx=` — is never polled by the DRM driver.
- Back-compat: `#[serde(default)]` ⇒ pre-S44 snapshots load as `Unknown` and
keep the note fallback so in-flight pendings reconcile across the upgrade;
a re-persisted legacy record gains the tag.
Plus docs/LIVE_BUY_RUNBOOK.md — the operator-driven procedure for a real
testnet buy (Grant→Act→Prove); deliberately OUT of the CI gate (the sandbox
has no funded wallet/network — the gate proves everything else via mocks).
Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed both money directions — no hostile HTTP pending is
DRM-resolvable, and no genuine DRM pending is stranded by mis-tagging, since a
money-bearing pending is un-reopenable so its tag is immutable):
- guardian F1 (ship-blocking, P12): fixed a KNOWN_GAPS self-contradiction
(the residual index said 2d open while the cell marked it CLOSED) + the
stale row title.
- guardian F2 / red-team F1: made `rail()` a REQUIRED trait method (removed
the default) so "every new record is positively tagged" holds by
construction, not by a doc comment — 7 impls updated.
- guardian F3 / red-team F5: ratcheted the reopen-adopts-rail behavior
(and that a money-bearing pending's tag is immutable ⇒ no stranding) +
an e2e assertion that the production pay closure stamps `Drm`.
- guardian F4 / red-team F2: a Drm-tagged pending with no `drm:tx=` note is
now visible (a one-time WARN) instead of silently folded into the
never-mining set; ratcheted (confirmer never called, money unchanged).
- red-team F4: documented the forward-incompatible-downgrade constraint on
`PaymentRail` (unknown rail string fails load fail-closed, no serde(other)).
- guardian F5/F6: qualified the S35 round-trip comment and marked
`seed_pendings` as deliberately exercising the legacy fallback.
Gate: server lib 1268 (default) / 1273 (dev-modes), runtime 434, common 96 —
all green; clippy clean on the touched regions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…chain scaffold
The live-money last mile, in two honest halves. The sandbox has no funded
wallet or network, so a real on-chain buy CANNOT run here — and nothing in
this change claims one did. What ships:
(A) GATE-RUNNABLE (runs every push) — two tests in `verify_receipt_cmd` prove a
receipt carrying a DRM settlement `rail_ref` in its signed `CapabilityUse`
is an admissible artifact through the ACTUAL standalone `verify-receipt`
CLI evaluator: written to disk as JSON, read back, verified AUTHENTIC
(exit 0) with the pinned signer, the `drm:tx=` reference surviving the
round-trip; and a byte-tampered settlement tx breaks the record-hash
recompute ⇒ INVALID (exit 1). This is the auditor's artifact path, beyond
the in-memory `verify_mandate_receipt` the S35 e2e already covers.
(B) NETWORK-GATED (operator/CI only) — a new `live-chain` cargo feature gates
`tests/live_drm_buy.rs`, an `#[ignore]`d integration test that drives the
real `ChainDrmMarketplace` (resolve → on-chain price gate → sign → broadcast
→ confirm) per docs/LIVE_BUY_RUNBOOK.md. Triple-fenced from the gate: the
whole file is `#![cfg(feature = "live-chain")]`, the fn is `#[ignore]`d, and
CI never enables the feature. It asserts the buy is HELD at broadcast
(`Indeterminate` with a real tx, never charged — the S35 invariant) then
confirms on-chain.
Council fold (both seats SHIP-WITH-FIXES; both verified by execution that the
gate tests are load-bearing, the live test cannot leak into the gate — proven
three ways — and NOTHING claims a live buy executed):
- red-team F1: aligned the gate fixture's emit shape to the real reconciler
(`Action::Execute` on `elastos://runtime/pay/<payee>`) and softened the
"exact shape" wording so the fixture doesn't over-claim production fidelity.
- both F2: reworded the live test's doc from "the whole stack / never refund
a sent tx" to the honest provider→chain leg it actually drives; the
ledger/reconcile/receipt legs are gate-proven against mocks + driven live by
the gateway per runbook §2.
- red-team F3: added a network-free CI `cargo check --features live-chain
--tests` step so a rename can't silently rot the operator's only live tool.
- guardian F1: assert `ELASTOS_DDRM_BUY_SIGN=wallet` up front (equally
mandatory with the empty subject) so a misconfig fails with the true cause.
- red-team F4: assert the rail_ref carries a real 0x+64-hex EVM tx before
polling, so a stub binary can't green the confirm loop on garbage.
- red-team F5: pin the tamper failure to `!hashes_ok` (not merely
`!structurally_valid`) — the mechanism is now a ratchet.
- guardian F3 / red-team F7: documented that a live re-run RE-SPENDS (settle
ignores the idempotency key; the standalone test bypasses ledger dedup) in
the runbook + the timeout assert, and that a `live-chain` build (pulls in
dev-modes) must never be reused as a release artifact.
- guardian F6: KNOWN_GAPS + runbook now state plainly this has NOT yet been
run against a live testnet in this repo (record the first run's tx hash).
Gate: server lib 1268 (default) / 1273 (dev-modes), bin 105 (+2 CLI ratchets),
runtime 434, common 96 — all green; live-chain test compiles + is correctly
ignored (0 run in the gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ic delta) CI declares `cargo fmt --all -- --check` but the tree had drifted (344 diff sites across 24 files) — a gate that cannot pass is a lie the honesty regime cannot carry (S46 council Track B4). This is `cargo fmt --all` verbatim: purely mechanical reflow, zero semantic change, proven by the unchanged test counts on the follow-up commit's full gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…leg ratchet, spawn_blocking)
Three council carry-overs closed, continuing the drive to 10/10 while the live
Base run waits for the operator:
- B1 (P16, council S43 guardian F4 partial): `capsule_watchdog::spawn_grouped`
— the single seam every provider/sidecar spawn goes through — now strips
RUNTIME_ONLY_SECRETS from the child env: ELASTOS_DDRM_BUY_SIGNED_TX (a
fully broadcastable signed tx; leaked to a hostile provider binary it could
be broadcast out-of-band while the read leg fails "pre-broadcast") and
ELASTOS_PAYMENT_TOKEN (the HTTP rail bearer). Both are consumed exclusively
in-process and passed onward as explicit op arguments, so no capsule has a
legitimate use for the env copy. Ratchet:
`runtime_only_secrets_are_stripped_from_spawned_capsules` (canary secrets
ABSENT in the child; ordinary ELASTOS_* config still passes through).
Honest scope: a targeted denylist of runtime-only secrets — the full
per-capsule env allowlist remains the stronger tracked hardening.
- B2 (closes council S43 guardian F2): the dedicated PREPARE-leg ratchet
`buy_authority::a_chain_prepare_deadline_types_the_buy_as_pre_broadcast` —
a chain stub answers the LISTING read then hangs only on
`prepare_transaction`; the buy is killed at the deadline and typed
`BuyError::PreBroadcast` (refund), carrying the SAME CHAIN_DEADLINE_MARKER
the SEND leg holds as Indeterminate — the call site decides, not the bytes.
Guards the ordering invariant a refactor could silently break (hoisting the
prepare out of the sign closure).
- B3 (closes council S42 guardian F8): `access_grant::prepare`/`assemble` in
viewer_open now run inside `tokio::task::spawn_blocking` — the
deadline-bounded (≤~31s) sidecar wait holds a blocking-pool thread, never
an async worker; a panicked task maps to the same fail-closed error arm.
- KNOWN_GAPS updated for all three closures (F4 marked PARTIALLY closed —
the allowlist stays tracked).
(The preceding commit 133c340 is the mechanical repo-wide `cargo fmt` that
makes CI's declared fmt gate truthful — B4.)
Gate: server lib 1269 (default) / 1275 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; clippy clean on touched files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ard + gate the dev-modes lane
The S46 council (red-team SHIP-WITH-FIXES + guardian SHIP-WITH-FIXES) proved
three of the sprint's claims outran the code; this folds all of them:
- red-team F1 / guardian F1 (HIGH, the blocker): "single spawn seam" was
FALSE — `ProviderBridge::spawn` (every general provider capsule), the
carrier service spawn, and the shell-capsule spawns (main/serve_cmd)
bypassed `spawn_grouped` and inherited the rail bearer under a normal
HTTP-rail config. The strip now lives as ONE shared list
(`elastos_runtime::provider::RUNTIME_ONLY_SECRETS`) applied at ALL FIVE
capsule seams; host-tool spawns (git/tar/self re-exec, which legitimately
keeps its env) are deliberately not stripped.
- red-team F5 (MEDIUM): the canary proved the helper strips, not that spawns
route through it. Added the SOURCE-STRUCTURAL guard
`every_command_spawn_site_is_a_known_seam_and_capsule_seams_strip_secrets`:
every `Command::new` site in elastos-server/elastos-runtime must be a
classified capsule-seam-carrying-the-strip or a consciously allowlisted
host tool — a NEW spawn path fails the gate until classified.
- guardian F2 (MAJOR, P12): `assemble_cached` (the popup-free re-open path)
still shelled the grant sidecar on the async executor one line below the
fix — now also in spawn_blocking, preserving its fall-back-to-enrolled arm.
- guardian F3 (MAJOR, gate honesty): the dev-modes construction ratchets
(S43 typed BuyError, the S46 prepare-leg deadline) were NEVER COMPILED by
the gate. `just _verify-tail` and ci.yml now both run the
`--features dev-modes` lib lane — a ratchet outside the gate cannot ratchet.
- guardian F4 (MINOR, accepted): the new test's env hygiene matches its S43
siblings' pattern; adopting the RAII EnvGuard across the ddrm test family
is noted follow-up work, not folded here.
- Both seats verified the fmt commit (133c340) mechanically reproducible
bit-for-bit, B2 (prepare-leg ratchet) rigorous, and B3 semantics preserved.
- KNOWN_GAPS updated to the now-true wording (all seams named, the false
first-cut claim recorded, the structural guard cited).
Gate: server lib 1270 (default) / 1276 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ps (Track C1)
The media/object viewer session stores were unbounded HashMaps: an entry —
holding MB-scale init bytes, sealed material, and an Arc to a live
KEY-AUTHORITY SUBPROCESS — lived until an explicit client close. Sustained
opens grew memory and process count without limit; an abandoned tab pinned a
child process forever. Worse (S47 guardian F2): /init, /cover, and the
manifests had NO expiry check, so an expired session's bytes were still
served (only segment/bytes/page reads failed closed).
New `api/session_bounds.rs` — ONE discipline both stores apply:
- SWEEP: every admission and every lookup removes all EXPIRED entries.
- CAP: at MAX_VIEWER_SESSIONS (256/store) evict soonest-to-expire live
sessions (TTL order, not LRU — fixed-window sessions, no per-read
bookkeeping, ungameable by touching your own sessions) with a warn.
- DEFERRED-DROP CONTRACT (the council crux, both seats HIGH/MEDIUM): a
session drop can reap a subprocess with ~1s grace; the sweep runs under
the store Mutex, so dropping in place would stall EVERY viewer's lookup
for N×1s on a mass sweep (worst case ~4 min with 255 hung authorities).
The helpers RETURN removed values; call sites (put/get/remove, both
stores) drop them after releasing the lock.
- Fail-closed: an evicted session's next read is "no such session" — the
viewer re-opens through the FULL authorization gate. Eviction can never
grant access (red-team confirmed; in-flight reads finish on their own
Arc clone).
Council fold (both seats SHIP-WITH-FIXES; the blocker was pre-folded):
- F1 (HIGH): deferred off-lock drops — including the pre-existing
remove_*_session single-drop.
- G-F2 (P12): module docs now credit the lookup-sweep with closing the
previously-unchecked /init//cover/manifest expiry (the change undersold
itself).
- RT-F2/G-F3: the process-global, principal-blind cap + the authorized-
opener precondition + per-principal fairness as future work — documented.
- RT-F6/G-F4: honest worst-case math on the cap (≈2.3 GB + ~512 procs/store
fully adversarial, resting on MAX_CAPSULE_LINE) replaces "the host
survives".
- G-F5a: the wiring test now inserts the expired session LAST, so the
LOOKUP sweep is provably the remover.
- G-F5b: the object store's missing wiring test recorded as an honest gap
(non-optional Arc<ObjectAuthorityProc> holds a real Child; wiring is
byte-identical to the tested media store).
- Clock/concurrency/off-by-one all verified consistent by the red team
(sweep and route guards share the same predicate; no serve-after-sweep
window; a backward clock step keeps MORE, never mass-sweeps).
Gate: server lib 1275 (default) / 1281 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…er contract (Track D1+D2)
The North Star breadth move: prove the market-provider seam is not DRM-shaped
by shipping a second chain-settled vertical behind it, and publish the
contract a third party could implement a provider from.
D1 — docs/SPEC-market-provider-v1.md: the versioned contract. The runtime owns
every money DECISION (mandate gate, meter, record-before-broadcast ledger,
receipts, reconciliation); a provider owns only the money REPORT (the
two-generals pay() classification + the compiler-required rail() tag + the
fail-safe confirmation reader). A conformance checklist maps each obligation
to its mechanical enforcement.
D2 — api/erc20_checkout.rs (`Erc20CheckoutProvider`): `runtime.pay
{payee: 0x…, amount}` becomes `transfer(payee, amount × ELASTOS_ERC20_SPEND_UNIT)`
on ONE operator-declared token:
- Typed by construction (the S43 discipline): bad address / unit overflow /
calldata / the wallet SIGN leg (incl. the chain PREPARE read) are
NotCharged — provably nothing moved; the broadcast op and after are
Indeterminate carrying `erc20:tx=<hash>;to=…;amount=…;tok=…`
(delimiter-stripped per component). A broadcast-accepted checkout is NEVER
charged until confirmed — Ok-at-broadcast does not exist.
- `rail() = PaymentRail::Erc20` (new variant; snake_case "erc20";
forward-compat posture per the S44 contract — a coordinated upgrade).
- Mock settlement: dev-modes + the explicit mock-money opt-in. Live: wallet
managed signing (dev-modes; a release build refuses NotCharged — the
external-signature checkout flow is a stated follow-up, same posture as
the DRM rail).
The chain-settled reconciler, generalized rail-STRICTLY:
- `parse_chain_tx(rail, note)`: Drm parses only `drm:tx=`, Erc20 only
`erc20:tx=` (a cross-rail note is a tx-less orphan — left Pending, warned,
never polled); the pre-S44 `Unknown` legacy fallback stays DRM-ONLY (it
never widens to new rails); Http never parses.
- `is_drm_pending` → `is_chain_settled_pending` ({Drm, Erc20} = ours);
ONE shared `confirm_chain_tx` (receipt + depth floor) both verticals use;
`Erc20CheckoutProvider` impls the confirmer via it, so the in-runtime
scheduler polls checkout pendings exactly like DRM buys.
Wiring — `ELASTOS_PAYMENT_RAIL=erc20` mirrors the DRM arm's fail-closed
discipline: ELASTOS_ERC20_TOKEN + ELASTOS_ERC20_SPEND_UNIT required (the cap
is a literal on-chain ceiling, never a silent 1-wei assumption); durable
meter/ledger required; mock mode refuses without ELASTOS_ALLOW_MOCK_PAYMENTS.
7 new ratchets: transfer-calldata words; junk payees refused pre-money;
unit-overflow refused pre-money; broadcast held-Indeterminate with a
parseable ref (e2e, dev-modes); Erc20 pendings reconciled + the S44 walls
hold for the new rail (hostile Http `erc20:tx=` never polled; Unknown legacy
stays DRM-only); parse_chain_tx rail-strict; the "erc20" serde round-trip.
Gate: server lib 1281 (default) / 1288 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift. Council review in flight; findings fold
as a follow-up commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ot warn
Council S48 red-team returned SHIP-CLEAN (all five money invariants confirmed
by construction: no charge-at-broadcast, no refund-after-send, the cap is a
literal ceiling, cross-rail steering impossible, the payee cannot forge
calldata). Two LOW footguns folded:
- LOW-2: ELASTOS_ERC20_MODE parse is now case-insensitive AND an
unrecognized non-empty value REFUSES TO WIRE (fail-closed) rather than
silently defaulting to Live — a typo'd mode must be visible, never a
surprise settlement mode. Empty/absent still defaults Live (the stricter
path).
- LOW-1: a boot-time warn when mock mode wires — mock settlements produce
tx hashes the LIVE confirmer can never find, so mock pendings hold their
reservations until manually resolved (money-safe over-hold, stated).
INFO-1 (the wallet-capsule trust posture on the live sign leg) is inherited
from the DRM rail unchanged and already documented there.
Gate: check clean, fmt 0 drift, server lib default 1281 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…r + wiring ratchet
Council S48 guardian returned SHIP-WITH-FIXES (the money core verified clean —
every leg typed by construction, no Ok path exists, the S44 walls hold and are
re-ratcheted for the new rail). All seven findings folded:
- F1 (HIGH, gate break): erc20_checkout's dev-modes-shadowed code left 5
clippy warnings in the default build, breaking the declared `-D warnings`
gate. Fixed with cfg-gated imports/helpers + a documented release-shape
dead_code allow. And went further (the S46 gate-truth rule): the WHOLE
workspace is now clippy-clean at 0 warnings — folded the pre-existing
drift too (wasm.rs too_many_arguments documented allow; session_bounds
deliberately-write-only field; parse_chain_tx lifetime elision;
intent_executor + bench doc-list indentation; market_quote's unit error
is now the named `ReadInFlight` type; carrier's 3 test env-lock guards
held across await get a justified allow — the lint targets production
deadlock risk, not test env serialization).
- F2 (MEDIUM, P11+P12): the rail selector is now CLOSED-WORLD — an
unrecognized non-empty ELASTOS_PAYMENT_RAIL refuses to wire (fail-closed)
instead of silently falling through to the HTTP/mock arms; the SPEC §6
"never degrades silently" sentence is now true of the selector itself.
- F3 (MEDIUM, P12): `erc20_rail_obeys_the_wiring_discipline` — the missing
wiring ratchet mirroring the DRM sibling: typo'd rail refuses; no token /
no unit / malformed unit / unrecognized mode / mock-without-opt-in all
refuse; fully-declared wires durable with the Erc20 tag + the confirmer.
(Takes the documented two-lock order — the first run caught an env race.)
- F4: SPEC row 4 reworded honestly (provider broadcast ratchet + the
shared-spine e2e via DRM, not a uniform "e2e").
- F5: the constructor's spend_unit clamp is now debug_assert'd + documented
(never silent).
- F6 (P12): wiring Live mode on a release build now warns at boot that
every checkout refuses until the external-signature flow ships.
- F7: SPEC §6 names the shared chain-config envs (the DDRM-era names; a
rail-neutral rename is tracked); the scheduler's "not the DRM rail"
doc/warn updated to "not a chain-settled rail".
Gate: server lib 1282 (default) / 1289 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; cargo clippy --workspace --all-targets:
ZERO warnings (the -D warnings gate passes truthfully).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…d by a conformance ratchet (Track D4)
docs/SPEC-mandate-v1.md: the byte-level formats behind Grant → Act → Prove,
written FROM the code so an independent party can implement verification with
no Flint code:
- §1 the trust model, stated without inflation (self-asserted signer — an
unpinned verification is NOT a trust decision; tamper-evident not
tamper-proof; the two completeness bounds).
- §2 the chained record + the exact hash preimage
(domain ‖ seq_be8 ‖ prev_hash32 ‖ event_json) + sig over the 32 RAW hash
bytes; the one canonicalization recipe (re-serialize the deserialized
event).
- §3 the receipt document; §4 the set-binding message byte layout (scope tag
byte, BE length prefixes, record hashes as 64-char ASCII hex).
- §5 the mandate events (INTERNALLY tagged via "type"; absent-when-unset
rules that keep old chains verifying; rail_ref formats cross-referenced to
SPEC-market-provider-v1).
- §6 the verification algorithm step by step + every verdict bit + the
fail-closed exit-code contract (0/1/3/4).
- §7 the signed intent + its preimage (domain WITH trailing NUL,
LITTLE-endian length prefixes — the endianness difference vs §2/§4 is
called out), and what the runtime enforces before executing.
- §8 versioning: frozen shapes, append-only optional fields, new tag for any
break.
The conformance ratchet `audit::tests::the_wire_format_matches_spec_mandate_v1`
pins the schema tag, both domain strings, every serialized key of the
document/record/scope/events, the timestamp shape, and that an exported
receipt verifies AUTHENTIC under §6. Writing it caught two real spec errors
before review (the event enum is internally tagged, and SecureTimestamp is
{unix_secs, monotonic_seq} — not the shapes first drafted), which is exactly
the failure mode the ratchet exists to prevent.
Gate: runtime 435 (+1), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings. Council review in flight; findings
fold as a follow-up commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hets
Council: guardian SHIP-WITH-FIXES (3 HIGH P12 defects), red-team
SHIP-WITH-FIXES (the crypto core sound — 3 domains separated, scope-tag byte
defeats binding replay, no path lets a self-signed forgery reach exit 0 —
but the spec under-specified canonicalization and had two over-claims). All
folded, some in CODE:
- guardian F1 (HIGH): §5 listed a phantom `capsule_id` on capability_revoke;
the real event is exactly {type, timestamp, token_id, reason}. Fixed the
spec AND added a byte-identity fixture for revoke (and grant) to the
conformance ratchet — revoke was the one §5 event the ratchet hadn't pinned,
which is exactly how the error slipped in.
- guardian F2 (HIGH) / red-team domain check: §7 cited a ratchet that only
pinned the timestamp segment. Added the known-answer ratchet
`the_intent_preimage_digest_is_frozen` — the WHOLE preimage (trailing-NUL
domain, field order, little-endian prefixes) as one fixed digest — and the
spec now cites it truthfully.
- guardian F3 (HIGH): event byte-ORDER was neither specified nor pinned. §5
now gives byte-exact compact-JSON templates (type first, declared order,
escaping rules) and the ratchet pins grant+revoke bytes.
- red-team F3 (MEDIUM, CODE): the receipt verifier used non-strict ed25519
while the intent path uses verify_strict — two conforming verifiers could
disagree on a malleated signature. Switched both receipt sig checks to
verify_strict; the spec's Primitives now mandates strict verification.
- red-team F1/F2/F4/F5/F7 + guardian F4/F6/F9: canonicalization caveat on the
"no Flint code" claim; the contiguous-end-truncation exception stated in §1;
"a PRESENT set_binding MUST verify for both scopes" (the dangerous
accept-a-tamper reading closed); "verify over the RECOMPUTED hash, never the
claimed"; §7 qualified (agent-key binding only when bound; intent_id replay
key vs the flint-<sig> payment key); grant-position-free + set_binding null
accepted; lowercase-hex + length-not-the-separator domain notes.
Gate: runtime 436 (+1 KAT), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Add the bounded-offer negotiation affordance: an agent under a pay-mandate makes an OFFER for a mandate-scoped asset and an injected Negotiator seller answers accept/counter/reject, closing the shop loop (quote -> negotiate -> pay). The provable property: the offer rides the signed input_hash as a canonical positive integer of spend units, and an offer above the mandate's un-spent cap (SpendMeter::remaining) is refused BEFORE it reaches the seller — an agent can never propose to commit its operator beyond granted authority. Non-value-moving by construction: it only reads the cap (no reserve, debit, or broadcast), so no money moves; settlement stays runtime.pay. A read-authority probe (like market_quote): it reconciles `read` and takes a read mandate on the pay resource — the dispatch gate authorizes only the fixed Action enum, so a made-up `negotiate` action could never be granted; the OFFER, not the action, makes it a proposal. The receipt attests the bounded offer, not the counterparty's disposition (which the runtime cannot verify); the seller's terms ride the response's disclosure channel as ephemeral market data. Production ListingNegotiator: a fixed-price DRM seller backed by the same read-only quote spine, reusing the buy gate's EXACT spend-unit conversion + pay-token guard (no second, divergent money-domain conversion). Wired only on the DRM rail (unwired on HTTP/ERC-20, which have no listing to negotiate against). Ratchet a_faithful_negotiate_returns_a_gate_legal_action_that_reconciles_matched pins the returned action to a gate-authorizable, declaration-matching value so the affordance can never regress to an action no token could authorize. Also fixes a stray SecureTimestamp clone the clippy -D warnings gate flagged in S49 test code. Full gate green: server lib 1300, dev-modes ok, bin 105, runtime 436, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…n + read-only seam contract Two-seat adversarial council: red-team SHIP-CLEAN (verified byte-identical buy-gate parity, cap-before-seller, non-value-moving, no report forgery); guardian SHIP-WITH-FIXES (3 MED + 2 LOW honesty/by-construction gaps). Both independently corroborated the pre-council action fix already landed. All folded: MED1 — the seller's rejection reason reaches the agent/logs but was unbounded (and embedded unsanitized chain-sourced token/price). The executor now bounds (<=200) and sanitizes (ascii-graphic + space) it, the same discipline the Performed report is held to. Ratchet a_hostile_seller_reject_reason_is_bounded_and_sanitized. MED2 — "reuses the buy gate's EXACT conversion" was by-comment, not by construction (a second, hand-synced copy). Extracted the ONE authorize_amount_against_listing (token guard + price parse + amount*spend_unit + cover boundary); both the DRM buy gate and the negotiate seller now call it, so an accept corresponds exactly to what the buy will pass. Buy-gate messages preserved (22 drm_marketplace tests green). MED3 — the read classification was honest only for the wired seller. Added a NORMATIVE contract to the Negotiator trait: implementations MUST be observationally read-only; an outbound-negotiating seller must reclassify the affordance action before being wired. LOW4 — the ratchet pinned a hand-copied action set. Added FromStr for Action (the one inverse of Display) and routed all three hand-rolled action matches (dispatch gate + two provisioning handlers) through parse_action; the ratchet now pins via FromStr. Round-trip test in token.rs. LOW5 — softened the panic-path comments to name the real residual: a seller that panics mid-read of the shared quote spine leaves a TTL-bounded (~30s) cache slot; money state stays clean and it is not agent-triggerable. Full gate green: server lib 1302, dev-modes 1309, bin 105, runtime 437, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hanged (Track C2) MEASURE-FIRST. Baseline (benches/audit_emit.rs, this box): memory-only emit 2.2 us/op (~446k ops/s); file-backed fsync-per-record ~950 us/op — a ~1.1k emits/s durable ceiling at ~430x the CPU cost, with every concurrent emitter serialized behind it. That fsync-under-the-chain-lock is the KNOWN_GAPS G8 blocker keeping ordinary grant/use events best-effort. THE REWRITE (three measured cuts; the first two were rejected by their own numbers): emit now appends in chain order under the chain lock (seq + sign + ordered write + BufWriter flush), publishes written_seq, then WAITS until a flusher's sync_all covers its seq — N concurrent emits share one fsync. - Cut 1 anchored the tail-truncation head anchor under the flush lock: 1.5x. - Cut 2 fsynced through the writer mutex: appenders (holding chain, waiting on writer) convoyed behind every ~1 ms fsync — batches collapsed to ~1 and 8 threads measured SLOWER than serial (725 vs 1057 ops/s). Rejected. - Final: the flusher fsyncs a CLONED fd (fsync commits the inode, not the fd), so appends proceed during the fsync; the anchor is guarded-monotone on its own mutex off the flush path. 8 threads: 3116 emits/s — 2.9x past the single-writer ceiling (pre-S51 they were pinned AT it). Single-threaded latency unchanged (one fsync each; nothing to coalesce). THE CONTRACT IS UNCHANGED AND HARDENED: Ok(()) still means THIS record is durably fsynced. A failed write/flush/fsync now POISONS the log — the failing emit and every later one return Err (fail-closed) — which also retires the pre-S51 latent hazard where retrying a failed seq could append a duplicate seq behind half-landed bytes and corrupt the chain for verifiers. The head anchor only ever records durable covers (never over-claims). Ratchets: concurrent_emits_group_commit_and_the_chain_stays_perfect (8x25 threads -> contiguous, signed, verify_chain-clean, anchor at full count, re-opens with_file_verified) and a_failed_durable_write_poisons_the_log_ fail_closed. Bench gains an 8-thread regime that verifies the chain after timing, so its number counts correct commits only. KNOWN_GAPS G8/perf notes updated: fail-closed grant/use is now a policy decision, not a blocked one. Lanes green at commit: runtime lib 439 (all 50 audit), server lib 1302, bin 105, common 96, fmt clean. dev-modes + clippy tail and the two-seat council run next; their findings land in the fold commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…usher unwind guard Two-seat adversarial council on the group-commit rewrite: both seats SHIP-WITH-FIXES, and both independently CONFIRMED the core invariants sound (Ok => durably fsynced; anchor never over-claims; flusher handoff liveness; Release/Acquire cover pairing; cloned-fd fsync semantics). All findings folded: F1 (guardian HIGH == red-team F2, the one real race): the poison gate was check-then-append — an emit queued on the chain lock during another emit's write failure could re-derive the FAILED seq and append behind its half-landed bytes (duplicate-seq corruption; fail-closed in every consequence but the chain bricked, and in the worst interleaving the racer's own record could be acknowledged on a never-verifiable chain). Closed by construction: the write-failure arm poisons while STILL HOLDING the chain lock, and emit re-checks poison UNDER the chain lock (chain->flush nesting verified acyclic). Ratchet: a_poisoned_log_refuses_before_assigning_a_seq_or_writing. F2 (both seats MED): "restart re-opens from the durable prefix" was claimed, not implemented — a crash-torn tail (provably never-acknowledged: an acknowledged record's full line+\n was fsynced) made the verified open REFUSE a healthy log, and S51's standing flushed-not-fsynced batch widened the window. Closed: quarantine_torn_tail at open moves a non-newline-terminated trailing fragment to <log>.torn-tail (preserved, never destroyed) and resumes from the intact prefix; safe because it cannot remove an acknowledged record and grants a tamperer nothing beyond the anchor floor they already had via a clean line-boundary cut. Mid-file corruption still refuses (tamper). Ratchet: a_torn_tail_is_quarantined_and_the_log_resumes_from_the_durable_prefix. F3 (guardian MED / red-team LOW): a panic-poisoned flush mutex could strand flushing=true (every later custody emit blocking forever — worse than fail-closed). All flush-lock sites now recover via PoisonError::into_inner (FlushState is single-statement-writes valid), a FlusherGuard clears+poisons on unwind, and the loud tracing moved outside the lock. F4 (guardian MED, resolved BY MEASUREMENT): the empty-anchor-after-crash brick is closed by treating an EMPTY anchor as absent (skip the floor for that open, loudly). The fold's first cut ALSO fsynced the anchor temp — and the bench rejected it: a single-threaded emitter is its own flusher, so it doubled the per-record cost (~950us -> ~1.7ms). Reverted with the honest rationale (the <=20-byte single-sector payload is complete-or-empty after a crash, never partial garbage; non-empty garbage stays fail-closed). F5 (guardian LOW): anchored_seq now seeds from max(resumed head, ON-DISK anchor) so an unverified reopen of a truncated log can never regress the anchor and destroy truncation evidence. F6 (guardian LOW): measured-number drift harmonized to the S51 decision run. RESIDUAL noted in KNOWN_GAPS: dual-writer same-file (no flock) is pre-existing; advisory flock is the tracked close. Post-fold measurements (uncontended): single-writer ~874 us/op (~1.1k/s, unchanged from pre-fold); 8-thread group commit 2947 emits/s (2.6x the single-writer ceiling). Full gate green: runtime 441 (52 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…acket Flint scope (a) AuditLog::with_file now holds an exclusive advisory flock on <log>.lock for the log's lifetime — the SAME single-opener discipline the spend meter and payment ledger hold (council S28 F4 lineage), closing the S51 red-team dual-writer residual on the LAST unprotected durable custody store. Two live instances on one file would keep independent chain heads, mint the same seqs, and corrupt the on-disk chain (and under the S51 group commit one instance's fsync covers the sibling's uncoordinated bytes); a second opener now fails WouldBlock fail-closed. Taken BEFORE the torn-tail quarantine so racing openers cannot both mutate the file. Ratchet: a_second_live_opener_of_the_same_log_is_refused (refusal + verified-open refusal + reopen-after-drop). The gateway's lazy audit-log constructor now serializes construction (its benign both-construct race would otherwise surface a spurious flock refusal); server_infra distinguishes the already-open-elsewhere refusal from tamper in its boot error (council LOW2). (b) The dev-only echo_stdout println moved OUTSIDE the chain lock (S51 red-team F4) — a blocked stdout can no longer serialize every emit — and now echoes only records that actually committed. (c) docs/AUDITOR_PACKET.md gains section 7: the Flint mandate/money plane as the second external-audit scope (the four invariant families to attack, the two wire-format specs, reproduction commands, and the honest note that the internal two-seat council reduces engagement cost but does not replace independent assurance). Deliberately NOT touched: live-buy env wiring (cosmetic renames deferred until after the operator's real-transaction test). Council (combined seat): SHIP-CLEAN — drop-order (lock releases after the writer flushes), O_CLOEXEC fd non-inheritance, the no-real-dual-opener sweep (CLI paths never open the live log file), double-checked OnceLock init, and the packet's invariant claims all verified against source; both LOW honesty notes folded (Unix-only qualifiers; the boot-error branch). Full gate green: runtime 442 (53 audit), server 1302, dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… opt-in ELASTOS_AUDIT_FAIL_CLOSED_USE=1 makes EVERY ordinary capability use-record in CapabilityManager::validate BLOCKING: an act whose signed use-record cannot be durably written is DENIED (ValidationError::AuditWriteFailed) — the same "no act without a durable record" discipline affordance-consent tokens have always had (W2 step 9). This is the G8 flip the S51 group commit unblocked (concurrent validates now share fsyncs, so the cost is ~1 fsync of latency, not a throughput collapse); it ships as an OPT-IN so the default stays byte-identical best-effort — no behavior change for existing deployments ahead of the operator's live-transaction test. - The switch parse is strictness-directional and pure-function-tested: unset/0/false => off, 1/true => on, GARBAGE => ON with a loud warn (a switch whose only job is to be stricter never silently weakens). - Env read once per constructor (new / load_or_generate / with_key); with_fail_closed_use(bool) sets the policy explicitly for tests/embedders. - KNOWN ORDERING BOUND documented (same as the affordance path always had): a use-limited token's count is consumed before the record lands, so a use burned on a failed record is spent without an act — fail-closed, never a free act. - Ratchets: the_fail_closed_use_opt_in_denies_undurable_acts_and_the_default_does_not (both directions against one failing-log seam) + the_fail_closed_use_switch_never_weakens_on_garbage. - KNOWN_GAPS G8 updated: the policy flip now EXISTS as an operator choice; the remaining default-ON decision is deliberately the operator's, not a code gap. Ordinary grants already have their fail-closed surface (grant_durable, S24). Runtime lanes green at commit time (runtime 444 incl. the 2 new ratchets, common 96, fmt clean); the server lanes + workspace clippy complete in the in-flight gate before push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…rd rationale
Combined council seat: SHIP-WITH-FIXES, doc-only — code semantics verified
correct, including that refunding a use on AuditWriteFailed would be UNSAFE
by the codebase's own BUG-4 contract (an Err emit may still have landed;
refunding could leave a durable success=true record the counter no longer
accounts for — burning keeps counter >= chain, the conservative direction).
F1 (MED): the poison-composition consequence was the load-bearing operational
fact and was stated nowhere an operator reads. Now stated plainly in the
field doc and KNOWN_GAPS: with the flag ON, one failed append/fsync poisons
the durable log and EVERY act process-wide denies until an operator restart
re-verifies the durable prefix — the deliberate posture (acts halt when the
custody plane is dead), and DoS-shaped (disk-full = capability-plane outage);
retries during the outage still burn use-limited tokens' counts.
F2 (LOW): the deny's own success=false use-record deliberately STAYS
best-effort under the flag — a deny is not an act, so a lost failure-record
is observability loss, never an unrecorded act; reasoned at
audit_validation_failure and in KNOWN_GAPS.
F4 (LOW): the memory-only claim hedged honestly ("short of a panic-poisoned
chain lock") + new ratchet the_fail_closed_use_opt_in_is_harmless_on_a_
memory_only_log (no durable writer => the flag adds no availability risk).
F3/F5: no code change — ordering bound already documented (F1's sentence adds
the retry-burn corollary); env-read-in-constructor cleared against precedent
with the with_fail_closed_use escape hatch.
Full gate green: runtime 445 (3 S53 ratchets), server 1302, dev-modes 1309,
bin 105, common 96; fmt clean; clippy -D warnings clean on the folded tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb # Conflicts: # elastos/crates/elastos-compute/src/providers/wasm.rs
…anes; gateway mounts the buy surface content_index: snapshot v2 carries scanned_to/backfill_low (v1 snapshots parse with defaults) + honest coverage labels (recent-window -> indexing -> indexed). gateway_marketplace: advance_index replaces rebuild-from-scratch — delta lane tracks head with a 120-block reorg overlap, backfill lane walks bounded windows toward the EventHub deploy block, a per-process poll loop advances every ELASTOS_MARKET_POLL_SECS; search capped at 200 newest-first. All reads stay getLogs via chain-provider (sole RPC declarant, P10); the money path still re-verifies live. gateway_cmd/gateway_entry: thread the runtime-built standing_service/capability_manager/pay_rail into the browser gateway so the Mandates/Money/Marketplace panels mount (auth-gated 401, previously 404). PHASE2_INDEX_AND_API.md records what landed and what stays open. Co-authored-by: Cursor <cursoragent@cursor.com>
…stion, status surface, alignment green - alignment: Mandates joins the fresh-passkey-AUTHENTICATION surfaces (Sprint 33 money writes; registration stays forbidden like Inbox/Wallet) — just alignment-check is green again on the branch. - advance_index gains scripted chain-provider stub tests: cold seed, contiguous delta, bounded backfill, fail-soft partial progress, reorg-overlap re-derive, deploy-block completion. - warm_listing_terms: each poll cycle refreshes the newest priced cards' briefs (PC2 parity), listing-terms TTL now tracks the poll interval so browsing hits warm cache. - content_index ingests legacy DigitalAssetRegistered alongside AssetCreated (one getLogs, topic0 alternatives); DAR rows carry the on-chain bytes16 KID. - GET /api/market/indexer-status: coverage/cursors/backfill%/row count, cache-only (never triggers a sweep). - DRM_MARKETPLACE_RAIL.md documents why gateway and serve both mount the buy surface. Co-authored-by: Cursor <cursoragent@cursor.com>
…rst status probe un-stalls backfill Co-authored-by: Cursor <cursoragent@cursor.com>
…uster pin-forward target kind; owned-copy count - /api/market/buy accepts the storefront payload (uri optional, quantity string-or-number) instead of 422ing - storefront POSTs the buy ONLY on explicit confirm, surfaces the gateway's verbatim refusal, polls acquire until on-chain access confirms (no more false "purchase complete") - availability-provider learns kind=pinning_service (IPFS Pinning Services API) with tls_server_name pinned-SNI verification — full chain validation against webpki roots, never disabled - market_get returns owned_balance (operative ERC-1155 id=1 balanceOf) so the UI can show "you hold N copies" Co-authored-by: Cursor <cursoragent@cursor.com>
Sanitized doc of the live geo-node layout (systemd, seed handling, carrier bridge — no addresses/credentials) and scripts/dev/dkms-docker: one ./up.sh stands up three isolated dkms-authority+carrier-node "servers" and assembles a carrier descriptor a runtime consumes via ELASTOS_DKMS_REMOTE — production shape on one machine, for engineer onboarding. Co-authored-by: Cursor <cursoragent@cursor.com>
…first EVM account
`resolve_subject_address` picked the FIRST `eip155:*` account the wallet-provider
returned, ignoring the principal's DEFAULT account. That is fine with a single linked
wallet, but once a second EVM account exists (e.g. a managed "Test EVM") it can sort
ahead of the user's default and hijack the on-chain subject — the gateway then binds the
open delegation to a wallet the browser is not connected with, and the client refuses to
sign ("the connected wallet ... is not the account linked to this content"). The wallet
UI and the browser wallet bridge already honor the default, so the two disagreed.
Resolve the default signable EVM account the SAME way the browser wallet bridge does
(default intents in priority order, newest `set_at` wins, the referenced account must be
a linked signable EVM account), falling back to the first linked EVM account only when no
usable default is recorded. The subject now follows the wallet the user actually connects
with, and adding further accounts can no longer hijack it.
The selection is factored into a pure `select_subject_from_accounts` so it is unit-tested
without the provider. Tests pin the regression (two EVM accounts, default second),
newest-default-wins, the no-default fallback, a default pointing at an unsignable account,
and the empty/fail-closed case.
`ELASTOS_DDRM_SUBJECT` remains the explicit override.
Gate: elastos-server lib 1311 passed / 0 failed.
…d metadata fetches + background warming An unresolvable metadata.json CID used to hold the SERIAL ipfs-provider pipe for ~10min (unbounded cat), and discovery awaited up to 24 of them per request — search/detail views hung for minutes once the index backfilled into dead mints. Now: content fetch accepts a per-request bound (forwarded to the provider's existing timeout_ms), the marketplace's interactive fetches pass tight bounds, and request-path enrichment merges CACHE HITS ONLY — uncached rows warm in the background (single-flight, negative-cached) and fill in on a later paint. Measured: /api/market/search 120s+ timeout -> 1-3s. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.